<?php

declare(strict_types=1);

namespace App\CustHub\Service;

use App\Common\Robot\Contracts\GatewayInterface;
use App\Common\Robot\Robot;
use App\Common\Service\BatchUpdateService;
use App\CustHub\Constants\SyncTaskStatusConstants;
use App\CustHub\Mapper\{
    ChRobotMapper,
    ChRoomMapper,
    ChRoomMemberMapper,
    ChSyncTaskMapper
};
use App\CustHub\Model\{ChContact, ChRobot, ChRoom, ChRoomMember, ChSyncTask};
use App\Setting\Service\SettingConfigService;
use Carbon\Carbon;
use FriendsOfHyperf\Lock\Exception\LockTimeoutException;
use Hyperf\Collection\Collection;
use Hyperf\Coroutine\Coroutine;
use Hyperf\Database\Model\Model;
use Hyperf\Di\Annotation\Inject;
use Psr\Log\LoggerInterface;
use function FriendsOfHyperf\Lock\lock;
use function Hyperf\Collection\collect;
use function Hyperf\Coroutine\co;

class ChRoomSyncService
{
    private const LOCK_TIMEOUT = 30;
    private const LOCK_BLOCK_TIMEOUT = 20;
    private const SYNC_DELAY_RANGE = [1, 3];
    private const BATCH_SIZE = 100;

    private const SYNC_ROOM_MEMBER_KEY = 'sync_room_member_key:';

    #[Inject]
    protected ChRobotMapper $robotMapper;

    #[Inject]
    protected ChSyncTaskMapper $syncTaskMapper;

    #[Inject]
    protected ChRoomMapper $roomMapper;

    #[Inject]
    protected ChRoomMemberMapper $roomMemberMapper;

    #[Inject]
    protected SettingConfigService $settingConfigService;

    #[Inject]
    protected BatchUpdateService $batchUpdateService;

    #[Inject]
    protected LoggerInterface $logger;

    protected GatewayInterface $robotApiService;



    /**
     * 群同步
     * @param ChSyncTask $task
     * @param int $currentSeq
     * @return void
     */
    public function syncRoom(ChSyncTask $task, int $currentSeq): void
    {
        try {
          co(function () use ($task, $currentSeq){
              $this->initializeForTask($task);

              if (!$this->validateRobotStatus($task)) {
                  return;
              }

              $this->processRoomSync($task, $currentSeq);
              $this->updateSyncTaskSuccess($task->id, $currentSeq);
          });
        } catch (\Throwable $e) {
            $this->handleSyncError($task, $e);
        }
    }

    /**
     * 根据任务初始化机器人
     * @param ChSyncTask $task
     * @return void
     */
    private function initializeForTask(ChSyncTask $task): void
    {
        $robot = $this->robotMapper->findById($task->robot_id);
        $this->robotApiService = make(Robot::class, [
            $robot->server_id,
            $robot->uuid
        ])->gateway();
    }

    private function validateRobotStatus(ChSyncTask $task): bool
    {
        $robot = $this->robotMapper->findById($task->robot_id);

        if (!$robot->login_status) {
            $this->markTaskFailed(
                $task->id,
                '企微号离线',
                SyncTaskStatusConstants::FAILURE
            );
            return false;
        }

        return true;
    }

    /**
     * 处理群同步
     * @param ChSyncTask $task
     * @param int $currentSeq
     * @return void
     */
    private function processRoomSync(ChSyncTask $task, int $currentSeq): void
    {
        $this->syncViaSessions($task, $currentSeq);
        $this->syncViaRoomList($task);
    }

    /**
     * 同步我的客户群
     * @param ChSyncTask $task
     * @return void
     */
    private function syncViaRoomList(ChSyncTask $task): void
    {
        $nextStartIndex = 0;
        do {
            $roomData = $this->robotApiService->getRoomList($nextStartIndex);
            if ($roomData === false) {
                break;
            }

            $roomList = $roomData['room_list'];
            $roomIds = collect($roomList)->pluck('room_id')->toArray();
            $this->randomDelay();
            $roomDetails = $this->robotApiService->getRoomDetail($roomIds);
            if($roomDetails !== false){
                $this->processRoomBatch(
                    $task,
                    $roomList,
                    $roomDetails
                );
            }
            $nextStartIndex = $roomData['has_more']
                ? $roomData['next_start_index']
                : -1;

        } while ($nextStartIndex > 0);
    }

    private function processRoomBatch(
        ChSyncTask $task,
        array $roomList,
        array $roomDetails
    ): void {
        $details = Collection::make($roomDetails);
        foreach ($roomList as $room){
            $detail = $details->firstWhere('room_id', $room['room_id']);
            $this->saveRoomWithLock(
                $task,
                array_merge($room, $detail ?? [])
            );
            $this->randomDelay();
        }
    }

    /**
     * 通过会话同步群聊
     * @param ChSyncTask $task
     * @param int $currentSeq
     * @return void
     */
    private function syncViaSessions(ChSyncTask $task, int $currentSeq): void
    {
        while (true) {
            $result = $this->robotApiService->getSessionPage($currentSeq);
            if (empty($result) || empty($result['session_list'])) {
                break;
            }
            $this->processSessionBatch(
                $task,
                $result['session_list'],
            );

            $currentSeq = $result['current_seq'];
            if (!$result['has_more'] || $currentSeq == 0) {
                break;
            }
        }
    }

    private function processSessionBatch(
        ChSyncTask $task,
        array $sessions,
    ): void {
        Collection::make($sessions)
            ->filter(fn($s) => $s['session_type'] === 1)
            ->chunk(self::BATCH_SIZE)
            ->each(function ($batch) use ($task) {
                $this->randomDelay();
                $roomDetails = $this->robotApiService->getRoomDetail(
                    $batch->pluck('session_id')->toArray()
                );
                if($roomDetails !== false){
                    $this->processRoomDetails(
                        $task,
                        $roomDetails
                    );
                }
            });
    }

    private function processRoomDetails(ChSyncTask $task, array $roomDetails): void
    {
        Collection::make($roomDetails)->each(function ($detail) use ($task) {
            if(!empty($detail['member_list']) && count($detail['member_list']) > 1){
                $this->saveRoomWithLock($task, $detail);
                $this->randomDelay();
            }
        });
    }

    private function saveRoomWithLock(ChSyncTask $task, array $roomDetail): void
    {
        $lockKey = sprintf(
            'sync_room_lock:%s:%s',
            $roomDetail['room_id'],
            $task->main_uid
        );
        $lock =  lock($lockKey, self::LOCK_TIMEOUT);
        try {
            $lock->block(self::LOCK_BLOCK_TIMEOUT, function () use ($task, $roomDetail) {
                $this->syncToDatabase($task, $roomDetail);
            });
        }catch (\Throwable $e){
            if($e instanceof LockTimeoutException){
                $this->logger->error('请求锁超时'.$e->getMessage());
            }else{
                $this->handleSyncError($task, $e);
            }
        } finally {
            $lock->release();
        }
    }

    private function syncToDatabase(ChSyncTask $task, array $detail): void
    {
        if(empty($detail['room_id'])){
            return;
        }
        $room = $this->findByRoomWxid(
            $task->main_uid,
            (string)$detail['room_id']
        );

        $this->persistRoomData($room, $task, $detail);

        $syncRoomKey = self::SYNC_ROOM_MEMBER_KEY.Carbon::now()->toDateString().':'.$detail['room_id'];
        if(!redis()->exists($syncRoomKey)){
            $this->syncMembers($task, $detail);
            redis()->set($syncRoomKey, 1, 15 * 60);
        }
    }

    private function findByRoomWxid(int $mainUid,string $roomWxid): ChRoom|Model|string|\Hyperf\Database\Model\Builder|null
    {
        return $this->roomMapper->model::query()
            ->where('main_uid',$mainUid)
            ->where('room_wxid',$roomWxid)
            ->first();
    }

    public function saveRoom(GatewayInterface $service, array $roomDetail, int $uid, int $mainUid, int $robotId): void
    {
        $this->robotApiService = $service;

        $task = new ChSyncTask();
        $task->uid = $uid;
        $task->main_uid = $mainUid;
        $task->robot_id = $robotId;
        $this->saveRoomWithLock($task,$roomDetail);
    }

    public function addRoomMember(GatewayInterface $service,int $uid,int $mainUid,int $robotId,string $roomId, string $createUserId, array $memberList): void
    {

        $this->robotApiService = $service;

        $roomMemberMap = ChRoomMember::query()
            ->where('room_wxid', $roomId)
            ->pluck('id', 'user_id');

        $task = new ChSyncTask();
        $task->uid = $uid;
        $task->main_uid = $mainUid;
        $task->robot_id = $robotId;

        $memberList = $this->prepareMemberBatch(
            $task,
            $roomId,
            $createUserId,
            collect($memberList)
        );

        $toUpdate = $memberList->filter(fn($user) => $roomMemberMap->has($user['user_id']));
        $toInsert = $memberList->reject(fn($user) => $roomMemberMap->has($user['user_id']));

        if ($toUpdate->isNotEmpty()) {
            ChRoomMember::upsert($toUpdate->toArray(), ['room_wxid','user_id']);
        }

        if ($toInsert->isNotEmpty()) {
            ChRoomMember::insert($toInsert->toArray());
        }
    }

    private function persistRoomData(
        ?ChRoom $room,
        ChSyncTask $task,
        array $detail
    ): void {
        $data = $this->prepareRoomData($task, $detail);
        if ($room) {
            $this->roomMapper->update($room->id, $data);
        } else {
            $room = $this->roomMapper->create($data);
        }

        $this->associateRobot($room, $task->robot_id);
        $this->roomMapper->clearCacheById($room->id);
        $this->roomMapper->clearCacheByRoomWxId($room->room_wxid,$room->main_uid);
        $this->roomMapper->clearCacheByRobotIdAndRoomWxid($room->room_wxid,$task->robot_id);
    }

    public function syncRoomBySession(ChRobot $robot,string $roomId): void
    {
        $room = $this->findByRoomWxid(
            $robot->main_uid,
            $roomId
        );
        if(!empty($room)){
            $this->associateRobot($room,$robot->id);
        }else{
            try {
                $robotApiService = make(Robot::class,[$robot->server_id,$robot->uuid])->gateway();
                $roomDetailList = $robotApiService->getRoomDetail([$roomId]);
                if($roomDetailList !== false){
                    $roomDetail = array_shift($roomDetailList);
                    $count = count($roomDetail['member_list'] ?? []);
                    if(empty($roomDetail['room_id']) || $count == 1){
                        return;
                    }
                    $task = new ChSyncTask();
                    $task->uid = $robot->uid;
                    $task->main_uid = $robot->main_uid;
                    $task->robot_id = $robot->id;
                    $roomData = $this->prepareRoomData($task,$roomDetail,$robotApiService);
                    $room = $this->roomMapper->create($roomData);
                    $this->associateRobot($room, $task->robot_id);
                    //同步群成员
                    co(function () use ($task,$roomDetail) {
                        $this->syncMembers($task,$roomDetail);
                    });
                    $this->roomMapper->clearCacheById($room->id);
                    $this->roomMapper->clearCacheByRoomWxId($room->room_wxid,$room->main_uid);
                    $this->roomMapper->clearCacheByRobotIdAndRoomWxid($room->room_wxid,$task->robot_id);
                }
            }catch (\Throwable $e){
                $this->logger->error('根据会话创建群聊失败',[
                    'error' => $e->getMessage(),
                    'room_id' => $roomId,
                    'robot_id' => $robot->id
                ]);
            }
        }
    }

    private function prepareRoomData(ChSyncTask $task, array $roomDetail,?GatewayInterface $robotApiService = null): array
    {
        $roomDetail['room_wxid'] = $roomDetail['room_id'];
        $roomDetail['uid'] = $task->uid;
        $roomDetail['main_uid'] = $task->main_uid;
        $roomDetail['room_name'] = !empty($roomDetail['room_name']) ? $roomDetail['room_name'] : '未设置群名称';
        $roomDetail['room_member_count'] = count($roomDetail['member_list'] ?? []);
        $roomDetail['robot_id'] = $task->robot_id;
        $roomDetail['create_user_id'] = $roomDetail['room_create_user_id'];
        $roomDetail['create_time'] = empty($roomDetail['room_create_time']) ? time() : $roomDetail['room_create_time'];
        $roomDetail['room_is_invite_confirmation'] = $roomDetail['room_enable_invite_confirm'];
        if(empty($roomDetail['room_avatar_url'])){
            if($roomDetail['room_is_invite_confirmation'] == 0){
                $this->randomDelay();
                if(is_null($robotApiService)){
                    $roomQrCode = $this->robotApiService->generateRoomQRCode($roomDetail['room_id']);
                }else{
                    $roomQrCode = $robotApiService->generateRoomQRCode($roomDetail['room_id']);
                }
                if($roomQrCode !== false && !empty($roomQrCode['room_headimg_url'])){
                    $roomDetail['room_avatar_url'] = $roomQrCode['room_headimg_url'];
                }else{
                    //设置默认头像
                    $roomDetail['room_avatar_url'] = $this->settingConfigService->getConfigByKey('room_default_avatar')['value'];
                }
            }else{
                //设置默认头像
                $roomDetail['room_avatar_url'] = $this->settingConfigService->getConfigByKey('room_default_avatar')['value'];
            }
        }
        $roomDetail['room_owner_time'] = $this->parseTimestamp($roomDetail['room_create_time']);
        return $roomDetail;
    }

    private function parseTimestamp($time): ?string
    {
        return empty($time) ? null : Carbon::createFromTimestamp($time)->toDateTimeString();
    }

    private function associateRobot(ChRoom $room, int $robotId): void
    {
        if (!$room->robots()->where('robot_id', $robotId)->exists()) {
            $room->robots()->attach($robotId);
        }
    }

    private function syncMembers(ChSyncTask $task, array $detail): void
    {
        if (empty($detail['member_list'])) {
            $this->handleEmptyMembers($detail['room_id']);
            return;
        }

        $this->processMembers(
            $task,
            $detail['room_id'],
            $detail['room_create_user_id'],
            $detail['member_list']
        );
    }

    private function handleEmptyMembers(string $roomWxid): void
    {
        $this->roomMemberMapper->markMembersAsDeleted($roomWxid);
    }

    private function processMembers(
        ChSyncTask $task,
        string $roomId,
        string $ownerId,
        array $members
    ): void {
        $processed = Collection::make($members)
            ->chunk(self::BATCH_SIZE)
            ->map(function ($batch) use ($task, $roomId, $ownerId) {
                $this->randomDelay();
                $rst = $this->prepareMemberBatch(
                    $task,
                    $roomId,
                    $ownerId,
                    $batch
                );
                return $rst;
            });
        $this->batchUpsertMembers($processed);
        $this->updateRoomOwner($processed, $roomId, $ownerId,$task->main_uid);
        $this->outRoom($processed,$roomId,$task->main_uid);
    }

    private function outRoom(Collection $processed,string $roomId,int $mainUid = 0)
    {
        $roomMemberUserIds =  ChRoomMember::query()
            ->where('room_wxid', $roomId)
            ->pluck('user_id')
            ->toArray();
        $userIds = $processed->flatMap(function ($item){
            return $item->pluck('user_id');
        })->toArray();
        $usersToDelete = array_diff($roomMemberUserIds, $userIds);

        // 如果有用户不存在，批量退群
        if (!empty($usersToDelete)) {
            ChRoomMember::query()
                ->where('room_wxid', $roomId)
                ->whereIn('user_id', $usersToDelete)
                ->where('is_out',0)
                ->update([
                    'is_out' => 1,
                    'out_time' => Carbon::now()->toDateTimeString()
                ]);
            //更新退群数量
            //$roomInfo = $this->roomMapper->findByRoomWxid($roomId,$mainUid);
            //container()->get(ChRoomCodeService::class)->saveRelationStatus($usersToDelete,$roomInfo->id,$mainUid,$roomInfo->uid,$roomId,2);
        }
    }

    private function prepareMemberBatch(
        ChSyncTask $task,
        string $roomId,
        string $ownerId,
        Collection $batch
    ): Collection {
        $userIds = $batch->pluck('user_id');
        $contacts = $this->getContactDetails($userIds);
        $corpIdList = $contacts->pluck('corp_id')->unique()->values()->toArray();
        $this->randomDelay();
        $corpList = $this->robotApiService->getCorpInfo($corpIdList);
        $corpMap = [];
        if($corpList !== false){
            $corpList = $corpList['corp_list'];
            $corpMap = collect($corpList)->keyBy('corp_id')->toArray();
        }

        $contactMap = ChContact::query()
            ->where('robot_id',$task->robot_id)
            ->whereIn('user_id',$userIds)
            ->pluck('id','user_id')
            ->toArray();

        return $batch->map(function ($member) use (
            $task,
            $roomId,
            $ownerId,
            $contacts,
            $corpMap,
            $contactMap
        ) {
            $contact = $contacts[$member['user_id']] ?? [];

            $corpId = $contact['corp_id'] ?? 0;
            $corpInfo = $corpMap[$corpId] ?? [];
            $corpName = $corpInfo['corp_name'] ?? '';
            $corpFullName = $corpInfo['corp_full_name'] ?? '';
            $data = [
                ...$member,
                'room_wxid' => $roomId,
                'nickname' => $contact['nickname'] ?? '',
                'avatar' => $contact['avatar_url'] ?? '',
                'corp_id' => 0,
                'corp_wxid' => $corpId,
                'corp_name' => $corpName,
                'corp_full_name' => $corpFullName,
                'is_friend' => isset($contactMap[$member['user_id']]) ? 1 : 0,
                'is_owner' => (int)($member['user_id'] === $ownerId),
                'member_type' => $this->getMemberType($member['user_id']),
                'is_out' => 0,
                'out_time' => null,
                'deleted_at' => null,
            ];
            $this->roomMemberMapper->filterExecuteAttributes($data);
            return $data;
        });
    }

    private function getContactDetails(Collection $userIds): Collection
    {
        $result = $this->robotApiService->getContactDetail($userIds->toArray());
        return Collection::make($result['contact_list'] ?? [])->keyBy('user_id');
    }

    private function isExistingContact(string $userId, int $robotId): int
    {
        return (int)ChContact::where('user_id', $userId)
            ->whereHas('robot', fn($q) => $q->where('id', $robotId))
            ->exists();
    }

    private function getMemberType(string $userId): int
    {
        return str_starts_with($userId, '788') ? 1 : 0;
    }

    private function batchUpsertMembers(Collection $batches): void
    {
        $batches->each(function ($batch) {
            ChRoomMember::upsert(
                $batch->toArray(),
                ['room_wxid', 'user_id']
            );
        });
    }

    private function updateRoomOwner(
        Collection $processed,
        string $roomId,
        string $ownerId,
        int $mainUid
    ): void {
        $owner = $processed->collapse()
            ->firstWhere('user_id', $ownerId);
        if ($owner) {
            $this->roomMapper->updateByCondition(
                [
                    'main_uid' => $mainUid,
                    'room_wxid' => $roomId,
                ],
                [
                    'room_owner_name' => $owner['nickname'],
                    'corp_id' => $owner['corp_id'],
                ]
            );
        }
    }

    public function syncRoomMember(array $data): void
    {
        if(empty($data['room_wxid'])){
            return;
        }
        $roomWxid = $data['room_wxid'];
        $roomInfo = $this->roomMapper->findByRoomWxid($roomWxid,$data['main_uid']);
        if(empty($roomInfo)){
            return;
        }

        $robotInfo = $roomInfo->robots()->select(['id','login_status','server_id','uuid'])->where('login_status',1)->first();

        if (empty($robotInfo)) {
            return;
        }

        $this->robotApiService = make(Robot::class, [$robotInfo->server_id, $robotInfo->uuid])->gateway();
        //获取群详情
        $roomDetails = $this->robotApiService->getRoomDetail([$roomWxid]);
        if(empty($roomDetails)){
            return;
        }

        $roomDetail = array_shift($roomDetails);

        $task = new ChSyncTask();
        $task->uid = $roomInfo->uid;
        $task->main_uid = $roomInfo->main_uid;
        $task->robot_id = $robotInfo->id;
        $this->processMembers($task,$roomWxid,$roomDetail['room_create_user_id'],$roomDetail['member_list']);
    }

    private function updateSyncTaskSuccess(int $taskId, int $seq): void
    {
        $this->syncTaskMapper->update($taskId, [
            'extra' => ['current_seq' => $seq],
            'status' => SyncTaskStatusConstants::SUCCESS,
            'end_time' => time()
        ]);
    }

    private function handleSyncError(ChSyncTask $task, \Throwable $e): void
    {
        if(empty($task->id)){
            return;
        }
        $this->logger->error('Room sync failed', [
            'task_id' => $task->id,
            'error' => $e->getMessage(),
            'trace' => $e->getTraceAsString()
        ]);

        $this->markTaskFailed(
            $task->id,
            '系统错误: '.$e->getMessage(),
            SyncTaskStatusConstants::FAILURE
        );
    }

    private function markTaskFailed(
        int $taskId,
        string $reason,
        int $status
    ): void {
        $this->syncTaskMapper->update($taskId, [
            'status' => $status,
            'failure_reason' => $reason,
            'end_time' => time()
        ]);
    }

    private function randomDelay(): void
    {
        Coroutine::sleep(randomFloat(1, 2));
    }
}